import logging
import threading
from typing import List, Union
from pathlib import Path

import openstack
from openstack.connection import Connection
from zunclient.client import Client as ZunClient
from zunclient.v1.client import Client as Zun
from glanceclient.client import Client as GlanceClient
from glanceclient.v2.client import Client as Glance

from utils.crypto import decrypt_pw
from contextvars import ContextVar

from configs.settings import flask_config

logger = logging.getLogger(__name__)


class NotLoginException(Exception):
    pass


class AdminConnNotExist(Exception):
    pass


class UserConnNotFind(Exception):
    pass


class OpenStackExtension:

    lock = threading.Lock()

    def __init__(self, decrypt_pw_func=decrypt_pw, config=None):

        # 加解密方法
        self.decrypt_pw_func = decrypt_pw_func

        self.user_project_id: str = None
        self.user_ops_conn: Union[None, Connection] = None
        self.user_zun_client: Union[None, Zun] = None
        self.user_glance_client: Union[None, Glance] = None
        self.admin_conn: Union[None, Connection] = None
        self.admin_zun_client: Union[None, Zun] = None
        self.admin_glance_client: Union[None, Glance] = None

        self.user_auth_type = "password"
        self.user_auth = None
        self.admin_auth_type = "password"
        self.admin_auth = None

        self.call_admin = ContextVar("call_admin", default=False)

        if config:
            self.init_app(config)

    def init_app(self, config=None):

        self.user_auth_type = config.OPENSTACK["auth_type"]
        self.user_auth = config.OPENSTACK["auth"]
        # 此处适用于一个项目ID配置
        for val in config.USER_TO_OPENSTACK:
            self.user_project_id = val
            user_ops_conn = self.init_openstack(project_id=val)
            self.user_ops_conn = user_ops_conn
            self.user_zun_client = self.init_zun(user_ops_conn)
            self.user_glance_client = self.init_glance(user_ops_conn)

        admin_conf = config.ADMIN_OPENSTACK
        if not admin_conf:
            logger.error("本次服务启动未加载admin项目配置")
            self.admin_conn = None
            self.admin_zun_client = None
            self.admin_glance_client = None
        else:
            self.admin_auth_type = admin_conf["auth_type"]
            self.admin_auth = admin_conf["auth"]
            self.admin_conn = self.init_openstack(is_admin=True)
            self.admin_zun_client = self.init_zun(self.admin_conn)
            self.admin_glance_client = self.init_glance(self.admin_conn)

    @classmethod
    def init_zun(cls, ops_conn: Connection) -> Union[Zun, None]:
        try:
            return ZunClient(session=ops_conn.session)
        except Exception as e:
            logger.error(f"账号{ops_conn.auth['username']}初始化zun client失败", exc_info=e)
            return None

    @classmethod
    def init_glance(cls, ops_conn: Connection) -> Union[Glance, None]:
        try:
            return GlanceClient(2, session=ops_conn.session)
        except Exception as e:
            logger.error(f"账号{ops_conn.auth['username']}初始化glance client失败", exc_info=e)
            return None

    def init_openstack(self, project_id=None, is_admin=False):
        """
        根据客户定制配置虚拟化参数
        """
        if not is_admin:
            auth_type = self.user_auth_type
            # 兼容多个项目ID公用一套密码配置
            _config = self.user_auth.get(project_id, self.user_auth)
            auth_config = {
                **(self.decrypt_pw_func(_config) if self.decrypt_pw_func else _config),
                "project_id": project_id,
            }
        else:
            auth_type = self.admin_auth_type
            auth_config = (
                self.decrypt_pw_func(self.admin_auth)
                if self.decrypt_pw_func
                else self.admin_auth
            )

        try:
            from core.openstack_connect import connect
            openstack_conn = connect(auth=auth_config, auth_type=auth_type)
            # openstack_conn = openstack.connect(auth=auth_config, auth_type=auth_type)
        except Exception as e:
            logger.error("初始化openstack连接失败")
            raise e
        else:
            return openstack_conn

    @property
    def conn(self) -> Connection:
        _conn = self.admin_conn if self.call_admin.get() else self.user_ops_conn
        _conn = self.ping(self.user_ops_conn)
        return _conn

    @property
    def zun_conn(self) -> Zun:
        _ = self.conn
        return self.admin_zun_client if self.call_admin.get() else self.user_zun_client

    @property
    def glance_conn(self) -> Glance:
        _ = self.conn
        return self.admin_glance_client if self.call_admin.get() else self.user_glance_client

    def ping(self, conn: Connection):
        """校验openstack连接是否可用"""
        if not conn:
            if self.call_admin.get():
                raise AdminConnNotExist("虚拟平台管理员链接对象不存在")
            raise UserConnNotFind("当前登录用户对应项目链接对象未找到")
        try:
            conn.authorize()
        except Exception as e:
            logger.error("链接失效, 将重新生成链接", exc_info=e)
            with self.lock:
                if self.call_admin.get():
                    self.admin_conn = self.init_openstack(is_admin=True)
                    self.admin_zun_client = self.init_zun(self.admin_conn)
                    self.admin_glance_client = self.init_glance(self.admin_conn)
                    return self.admin_conn
                else:
                    self.user_ops_conn = self.init_openstack(self.user_project_id)
                    self.user_zun_client = self.init_zun(self.user_ops_conn)
                    self.user_glance_client = self.init_glance(self.user_ops_conn)
                    return self.user_ops_conn
        else:
            return conn


class OpenStackOperateHandler:
    """OpenStack操作快捷封装"""

    def __init__(self, conn: Connection):
        self.conn: Connection = conn

    def refresh_conn(self, conn: Connection):
        """为当前操作对象刷新链接"""
        self.conn = conn


class ZunOperateHandler:
    """OpenStack Zun containers 操作快捷封装"""

    def __init__(self, zun_client: Zun):
        self._zun_client = ContextVar("zun_client", default=None)
        self._zun_client.set(zun_client)

    def refresh_conn(self, client: Zun):
        """为当前操作对象刷新链接"""
        self._zun_client.set(client)

    @property
    def zun_client(self) -> Zun:
        return self._zun_client.get()

    def get_host_list(
        self,
        marker: str = None,
        limit: int = None,
        sort_key: str = None,
        sort_dir: str = None,
    ) -> List:
        """获取全部主机列表.
        :param marker: 主机UUID，配合limit分页.
        :param limit: None时返回zun api限制最大值，>0时返回对应数量
        :param sort_key: 排序字段
        :param sort_dir: 排序方式 'asc' (默认) 或 'desc'.
        """
        return self.zun_client.hosts.list(
            marker=marker, limit=limit, sort_key=sort_key, sort_dir=sort_dir
        )

    def get_container_list(
        self,
        marker=None,
        limit=None,
        sort_key=None,
        sort_dir=None,
        all_projects=False,
        **kwargs,
    ) -> List:
        """获取全部容器列表.
        :param marker: 主机UUID，配合limit分页.
        :param limit: None时返回zun api限制最大值，>0时返回对应数量
        :param sort_key: 排序字段
        :param sort_dir: 排序方式 'asc' (默认) 或 'desc'.
        :param all_projects: 从全部项目中获取.
        kwargs:
          - name: 按名称
          - image: 按镜像
          - project_id: 按项目ID
          - user_id: 按用户ID
          - memory: 按 Mib 中的内存大小过滤响应
          - host: 按主机名过滤响应，作为字符串
          - task_state: 按任务状态过滤响应
          - status: 按容器的当前状态过滤响应
          - auto_remove: boolean: 根据响应是否在退出时自动删除来过滤响应
        """
        return self.zun_client.containers.list(
            marker=marker,
            limit=limit,
            sort_key=sort_key,
            sort_dir=sort_dir,
            all_projects=all_projects,
            **kwargs,
        )

    def create_container(
        self,
        image: str = "",
        name: str = None,
        command: str = None,
        cpu: float = None,
        memory: float = None,
        workdir=None,
        labels=None,
        environment=None,
        restart_policy=None,
        interactive=None,
        tty=None,
        image_driver=None,
        security_groups=None,
        nets=None,
        runtime=None,
        hostname=None,
        auto_remove=None,
        auto_heal=None,
        availability_zone=None,
        hints=None,
        mounts=None,
        privileged=None,
        healthcheck=None,
        exposed_ports=None,
        host=None,
        entrypoint=None,
    ):
        """创建一个容器
        :param image: 镜像名或ID
        :param name: 容器名称
        :param command: 向容器发送命令
        :param cpu: 虚拟 CPU 的数量
        :param memory: 以 MiB 为单位的容器内存大小
        :param workdir: 运行命令的工作目录
        :param labels: 将标签映射添加到容器
        :param environment: 要在容器中设置的环境变量
        :param restart_policy: 重启策略 no, on-failure, always, unless-stopped
        :param interactive: 即使没有连接，也要保持 STDIN 打开
        :param tty: 此容器是否应该为自己分配一个 TTY
        :param image_driver: 拉取镜像方式，docker 或 glance
        :param security_groups: 安全组
        :param nets: 指定网络，不指定则默认新建一个网络
        :param runtime: 用于创建容器的容器运行时工具
        :param hostname: 容器的主机名
        :param auto_remove: 当容器的进程退出时，在守护进程端启用容器的自动删除
        :param auto_heal: 修复 docker 中不存在的容器的标志
        :param availability_zone: 运行容器的可用区。
                                  通常，管理员用户将使用可用区将容器主机安排到逻辑组中。
                                  可用区提供与其他可用区的物理隔离和冗余形式。
        :param hints: 要发送到调度程序的数据字典。
        :param mounts: 字典数据列表，用于指定如何将卷安装到容器中。容器将在创建时挂载卷。
                       每个项目都可以有一个type指定卷类型的属性。支持的卷类型是volume或bind。
                       如果未指定此属性，则默认为volume. 要使用绑定安装的预先存在的 Cinder 卷来配置容器，
                       请在source属性中指定卷的 UUID 或名称。或者，可以动态创建 Cinder 卷。在这种情况下，
                       需要在size属性中指定卷的大小。另一种选择是将用户提供的文件挂载到容器中。
                       在这种情况下，type属性应该是'bind'并且文件的内容包含在source属性。
                       卷将被挂载到容器的文件系统中，需要在destination 属性中指定挂载卷的路径。
        :param privileged: 特权容器
        :param healthcheck: 容器的健康检查字典。指定要执行的测试命令以检查容器是否健康。支持四个参数：
                            cmd：运行以检查运行状况的命令。
                            interval：运行检查之间的时间（以秒为单位）。
                            retries：需要报告不健康的连续失败。
                            timeout：允许一项检查运行的最长时间（以秒为单位）。
        :param exposed_ports: 一个字典数据的列表，用于指定如何暴露容器的端口。
                             如果指定了这个参数，Zun 将创建一个带有一组规则的安全组，
                             以打开应该被暴露的端口，并将安全组与容器相关联。
                             该值的形式是 {"<port>/<protocol>: {}"，
                             其中端口是容器的端口，协议是 tcp 或 udp。如果没有提供协议，将使用 tcp。
        :param host: 要在其上创建容器的主机的名称。如果没有找到具有给定主机名的主机，API 将返回 400。默认情况下，它只能由管理员指定。
        :param entrypoint: 覆盖图像默认入口点的入口点（）
        """
        kwargs = {param: value for param, value in locals().items() if value}
        kwargs.pop("self", None)
        return self.zun_client.containers.create(**kwargs)

    def update_container_quota(self, container_ident, cpu=None, memory=None, name=None):
        """更新容器配置
        :param container_ident: 容器标识/ID
        :param cpu: 字符串浮点类型 CPU 的数量
        :param memory: 以 MiB 为单位的容器内存大小
        :param name: 容器名称
        """
        patch_quota = {param: val for param, val in locals().items() if val is not None}
        patch_quota.pop("self", None)
        if not patch_quota:
            raise ValueError("容器配置更新参数为空")
        return self.zun_client.containers.update(container_ident, **patch_quota)

    def update_container_name(self, container_ident, new_name):
        """更新容器名称"""
        return self.zun_client.containers.rename(container_ident, new_name)

    def get_container_details(self, container_ident):
        """获取容器信息"""
        return self.zun_client.containers.get(container_ident)

    def get_container_stats(self, container_ident):
        """获取容器状态"""
        return self.zun_client.containers.stats(container_ident)

    def reboot_container(self, container_ident, timeout):
        """重启容器"""
        return self.zun_client.containers.restart(container_ident, timeout)

    def start_container(self, container_ident):
        """启动一个容器"""
        return self.zun_client.containers.start(container_ident)

    def stop_container(self, container_ident, timeout):
        """停止一个容器"""
        return self.zun_client.containers.stop(container_ident, timeout=timeout)

    def del_container(self, container_ident, stop=False, force=False):
        """删除容器
        - stop 先停止再删除
        - force 强制删除
        """
        return self.zun_client.containers.delete(container_ident, force=force, stop=stop)

    def kill_container(self, container_ident, signal=None):
        """杀死一个运行中容器"""
        return self.zun_client.containers.kill(container_ident, signal=signal)

    def pause_container(self, container_ident):
        """挂起一个容器"""
        return self.zun_client.containers.pause(container_ident)

    def unpause_container(self, container_ident):
        """解除挂起"""
        return self.zun_client.containers.unpause(container_ident)

    def rebuild_container(self, container_ident, image=None, image_driver=None):
        """重新构建某个容器"""
        return self.zun_client.containers.rebuild(
            container_ident, image=image, image_driver=image_driver
        )

    def put_file_to_container(self, container_ident, path, data):
        """上传文件到容器内"""
        return self.zun_client.containers.put_archive(container_ident, path, data)

    def get_file_from_container(self, container_ident, path):
        """从容器中获取某文件"""
        return self.zun_client.containers.get_archive(container_ident, path)

    def execute(self, container_ident, command, run=None, interactive=None):
        """在容器中执行命令
        :param container_ident: 容器标识/ID
        :param command: 执行的命令
        :param run: 是否运行命令。
               如果此参数设置为 true，Zun 将立即运行该命令。
               如果该参数设置为false，Zun 将不会运行该命令，而是返回必要的信息（即URL 和执行id）（触发执行URL）
        :param interactive: Keep STDIN open even if not attached, allocate a pseudo-TTY
        """
        kwargs = {"command": command}
        if run is not None:
            kwargs.update({"run": run})
        if interactive is not None:
            kwargs.update({"interactive": interactive})
        return self.zun_client.containers.execute(container_ident, **kwargs)

    def execute_resize(self, container_ident, exec_id, height, width):
        """在容器中执行命令时调整 tty 大小
        :param container_ident: 容器标识/ID
        :param exec_id: 实例ID
        :param height: tty高度
        :param width: tty宽度
        """
        return self.zun_client.containers.execute_resize(
            container_ident, exec_id, height, width
        )

    def get_logs(
        self,
        container_ident,
        timestamps=True,
        tail=None,
        since=None,
        stderr=True,
        stdout=True,
    ):
        """获取容器日志
        :param container_ident: 容器标识/ID
        :param timestamps: 日志是否跟随时间戳
        :param tail: 返回 tail 行日志, 默认获取所有
        :param since: 返回自 since 日期时间或整数时期以来所有日志
        :param stderr: 获取标准错误
        :param stdout: 获取标准输出
        """
        return self.zun_client.containers.logs(
            container_ident,
            timestamps=timestamps,
            tail=tail,
            stderr=stderr,
            stdout=stdout,
            since=since,
        )

    def get_top(self, container_ident, ps_args=None):
        """获取容器内正在运行进程
        ps_args: ps 命令参数
        """
        return self.zun_client.containers.top(container_ident, ps_args=ps_args)

    def network_list(self, container_ident):
        """获取该容器所有网络"""
        return self.zun_client.containers.network_list(container_ident)

    def network_detach(self, container_ident, network_id_or_name=None, port=None):
        """将容器从某网络分离"""
        if network_id_or_name and port:
            raise ValueError("network_id_or_name 与 port 互斥")
        kwargs = (
            {"network": network_id_or_name} if network_id_or_name else {"port": port}
        )
        return self.zun_client.containers.network_detach(container_ident, **kwargs)

    def network_attach(
        self, container_ident, network_id_or_name=None, port=None, fixed_ip=None
    ):
        """将容器添加到某网络
        :param container_ident: 容器标识/ID
        :param network_id_or_name: 要附加到容器的网络的ID或名称
        :param port: 你想与容器绑定的端口的ID或名称。network和port是相互排斥的。
                    如果你不指定端口参数，Zun将分配一个端口并将该端口绑定到容器上。
        :param fixed_ip: 固定IP地址。如果该容器没有该IP对应网络，则返回400
        """
        if network_id_or_name and port:
            raise ValueError("network_id_or_name 与 port 互斥")
        kwargs = (
            {"network": network_id_or_name} if network_id_or_name else {"port": port}
        )
        if fixed_ip:
            kwargs.update({"fixed_ip": fixed_ip})
        return self.zun_client.containers.network_attach(container_ident, **kwargs)

    def add_securtiy_group(self, container_ident, security_group: str):
        """为容器添加安全组"""
        return self.zun_client.containers.add_security_group(
            container_ident, security_group
        )

    def remove_security_group(self, container_ident, security_group):
        """移除安全组"""
        return self.zun_client.containers.remove_security_group(
            container_ident, security_group
        )

    def commit(self, container_ident, repository, tag=None):
        """将当前容器打包为一个新镜像
        repository: 容器镜像的存储空间
        tag: 可选 镜像标签
        """
        self.zun_client.containers.commit(container_ident, repository, tag=tag)


class GlanceOperateHandler:
    """Glance操作快捷封装"""

    def __init__(self, glance_client: Glance):
        self._glance_client = ContextVar("glance_client", default=None)
        self._glance_client.set(glance_client)

    def refresh_conn(self, client: Glance):
        """为当前操作对象刷新链接"""
        self._glance_client.set(client)

    @property
    def glance_client(self) -> Zun:
        return self._glance_client.get()

    def create_container_image(
            self,
            image_name: str,
            image_file_path: str,
            setup_cpu: int,
            setup_ram: int,
            other_tags: dict = None
    ):
        """创建镜像"""
        tags = ["=".join([str(key), str(val)]) for key, val in other_tags.items()] if other_tags else []
        tags.append(f"setup_cpu={setup_cpu}")
        tags.append(f"setup_ram={setup_ram}")

        image_file = Path(image_file_path)
        image = self.glance_client.images.create(
            name=image_name,
            disk_format="raw",
            container_format="docker",
            visibility="public",
            protected=False,
            tags=["=".join([str(key), str(val)]) for key, val in other_tags.items()]
        )
        self.glance_client.images.upload(image.id, image_data=image_file.open("rb"))
        return self.glance_client.images.get(image.id)

    def get_image_list(self, limit=None, page_size=None):
        """获取镜像列表(kvm与container镜像混合)"""
        return self.glance_client.images.list(limit=limit, page_size=page_size)

    def delete_image(self, image_id):
        self.glance_client.images.delete(image_id)

class OpenStackProxy:

    """代理flask openstack拓展, 限制属性访问, 允许在当前上下文切换openstack操作账户
    ops.call_admin()  切换到openstack admin账户链接操作 （若配置admin账户）
    """

    __slots__ = ("__ops__", "__ops_handler__", "__zun_handler__", "__glance_handler__")

    def __init__(self, config, decrypt_pw_func=decrypt_pw):
        self.__ops__ = OpenStackExtension(decrypt_pw_func=decrypt_pw_func, config=config)
        self.__ops_handler__: OpenStackOperateHandler = OpenStackOperateHandler(self.__ops__.conn)
        self.__zun_handler__: ZunOperateHandler = ZunOperateHandler(self.__ops__.zun_conn)
        self.__glance_handler__: GlanceOperateHandler = GlanceOperateHandler(self.__ops__.glance_conn)

    @property
    def conn(self) -> Connection:
        return self.__ops__.conn

    @property
    def ops_operate(self) -> OpenStackOperateHandler:
        """openstack操作封装"""
        conn = self.__ops__.conn
        self.__ops_handler__.refresh_conn(conn)
        return self.__ops_handler__

    @property
    def zun_operate(self) -> ZunOperateHandler:
        client = self.__ops__.zun_conn
        self.__zun_handler__.refresh_conn(client)
        return self.__zun_handler__

    @property
    def glance_operate(self) -> GlanceOperateHandler:
        client = self.__ops__.glance_conn
        self.__glance_handler__.refresh_conn(client)
        return self.__glance_handler__

    def call_admin(self):
        """切换到admin用户操作"""
        if self.__ops__.admin_conn:
            self.__ops__.call_admin.set(True)
            return
        raise ValueError("当前项目未配置openstack admin账户")

    def call_user(self):
        """切换到普通用户操作（默认使用用户账户操作）"""
        self.__ops__.call_admin.set(False)


ops: OpenStackProxy = OpenStackProxy(flask_config)
